D:\a\cssh-rs\cssh-rs\cssh-rs-core\src\utils\config.rs
Line | Count | Source |
1 | | //! Client and Daemon configuration structs. |
2 | | |
3 | | use serde_derive::{Deserialize, Serialize}; |
4 | | use std::env; |
5 | | use windows::Win32::System::Console::{ |
6 | | BACKGROUND_BLUE, BACKGROUND_INTENSITY, BACKGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, |
7 | | FOREGROUND_INTENSITY, FOREGROUND_RED, |
8 | | }; |
9 | | |
10 | | /// Behavior when an arrow / `hjkl` keystroke would move the |
11 | | /// enable/disable submenu's selection past the edge of the client grid. |
12 | | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] |
13 | | #[serde(rename_all = "snake_case")] |
14 | | pub enum EdgeBehavior { |
15 | | /// Keep the current selection on edge keystrokes (default). |
16 | | Clamp, |
17 | | /// Wrap to the opposite edge of the same row (Left/Right) or column |
18 | | /// (Up/Down). |
19 | | Wrap, |
20 | | } |
21 | | |
22 | | impl Default for EdgeBehavior { |
23 | 79 | fn default() -> Self { |
24 | 79 | return EdgeBehavior::Clamp; |
25 | 79 | } |
26 | | } |
27 | | |
28 | | /// Default console color applied when a client is in the |
29 | | /// `Disabled` state. |
30 | | /// |
31 | | /// Default-grey foreground (red+green+blue, no intensity) on a |
32 | | /// `BACKGROUND_INTENSITY`-only background paints the window as light text on |
33 | | /// a muted dark-grey background - a clear "this client is greyed out" cue |
34 | | /// that stays visually distinct from the daemon's bright-red palette. |
35 | | const DEFAULT_DISABLED_CONSOLE_COLOR: u16 = |
36 | | FOREGROUND_RED.0 | FOREGROUND_GREEN.0 | FOREGROUND_BLUE.0 | BACKGROUND_INTENSITY.0; |
37 | | |
38 | | /// Default console color for the daemon's currently selected submenu |
39 | | /// client: bright-white on blue, distinct from the daemon's bright-red |
40 | | /// and the muted disabled palettes so it stands out at a glance. |
41 | | const DEFAULT_HIGHLIGHTED_CONSOLE_COLOR: u16 = FOREGROUND_RED.0 |
42 | | | FOREGROUND_GREEN.0 |
43 | | | FOREGROUND_BLUE.0 |
44 | | | FOREGROUND_INTENSITY.0 |
45 | | | BACKGROUND_BLUE.0; |
46 | | |
47 | | /// Placeholder for the `<username>@<host>` argument to the chosen SSH program. |
48 | | const DEFAULT_USERNAME_HOST_PLACEHOLDER: &str = "{{USERNAME_AT_HOST}}"; |
49 | | |
50 | | /// Representation of the project configuration. |
51 | | /// |
52 | | /// Includes subcommand specific configurations for `client` and `daemon` subcommands |
53 | | /// as well es the cluster tags. |
54 | | #[derive(Serialize, Deserialize, Default, PartialEq, Debug)] |
55 | | pub struct Config { |
56 | | /// List of cluster tags. |
57 | | /// |
58 | | /// Includes the name of the cluster tag and a list of hostnames. |
59 | | pub clusters: Vec<Cluster>, |
60 | | /// Configuration relevant for the `client` subcommand. |
61 | | pub client: ClientConfig, |
62 | | /// Configuration relevant for the `daemon` subcommand. |
63 | | pub daemon: DaemonConfig, |
64 | | } |
65 | | |
66 | | /// Representation of the project configuration |
67 | | /// where everything is optional. |
68 | | /// |
69 | | /// Used to handle cases where only some or none of the configurations are present. |
70 | | /// Enables backwards compatiblity with configuration files written by older versions. |
71 | | #[derive(Serialize, Deserialize, Default)] |
72 | | pub struct ConfigOpt { |
73 | | #[allow(missing_docs)] |
74 | | pub clusters: Option<Vec<Cluster>>, |
75 | | #[allow(missing_docs)] |
76 | | pub client: Option<ClientConfigOpt>, |
77 | | #[allow(missing_docs)] |
78 | | pub daemon: Option<DaemonConfigOpt>, |
79 | | } |
80 | | |
81 | | impl From<ConfigOpt> for Config { |
82 | | /// Unwraps the existing configuration values or applies the default. |
83 | 16 | fn from(val: ConfigOpt) -> Self { |
84 | 16 | return Config { |
85 | 16 | clusters: val.clusters.unwrap_or_default(), |
86 | 16 | client: val.client.unwrap_or_default().into(), |
87 | 16 | daemon: val.daemon.unwrap_or_default().into(), |
88 | 16 | }; |
89 | 16 | } |
90 | | } |
91 | | |
92 | | impl From<Config> for ConfigOpt { |
93 | | /// Wraps all configuration values as options. |
94 | 1 | fn from(val: Config) -> Self { |
95 | 1 | return ConfigOpt { |
96 | 1 | clusters: Some(val.clusters), |
97 | 1 | client: Some(val.client.into()), |
98 | 1 | daemon: Some(val.daemon.into()), |
99 | 1 | }; |
100 | 1 | } |
101 | | } |
102 | | |
103 | | /// Representation of a cluster tag. |
104 | | #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] |
105 | | pub struct Cluster { |
106 | | /// Name of the cluster tag, used to identify it. |
107 | | pub name: String, |
108 | | /// List of hostnames the cluster tag is an alias for. |
109 | | pub hosts: Vec<String>, |
110 | | } |
111 | | |
112 | | /// Representation of the `client` subcommand configurations. |
113 | | #[derive(Serialize, Deserialize, PartialEq, Debug)] |
114 | | pub struct ClientConfig { |
115 | | /// Full path to the SSH config. |
116 | | /// |
117 | | /// # Example |
118 | | /// |
119 | | /// `'C:\Users\<username>\.ssh\config'` |
120 | | pub ssh_config_path: String, |
121 | | /// Name of the program used to establish the SSH connection. |
122 | | /// |
123 | | /// Note specific to `cmd.exe`: a relayed Ctrl+C interrupts a running command |
124 | | /// but cannot reprint its idle prompt (which needs a real focused keypress). |
125 | | /// # Example |
126 | | /// |
127 | | /// `'ssh'` |
128 | | pub program: String, |
129 | | /// List of arguments provided to the program. |
130 | | /// |
131 | | /// Must include the `username_host_placeholder`. |
132 | | /// |
133 | | /// # Example |
134 | | /// |
135 | | /// `['-XY', '{{USERNAME_AT_HOST}}']` |
136 | | pub arguments: Vec<String>, |
137 | | /// Placeholder string used to inject `<user>@<host>` into the list of arguments. |
138 | | /// |
139 | | /// # Example |
140 | | /// |
141 | | /// `'{{USERNAME_AT_HOST}}'` |
142 | | pub username_host_placeholder: String, |
143 | | /// Controls back- and foreground colors of the client console window |
144 | | /// when the client is in the `Disabled` state. |
145 | | /// |
146 | | /// Uses the same encoding as [`DaemonConfig::console_color`]. |
147 | | /// All [standard Windows color combinations][1] are available: |
148 | | /// |
149 | | /// FOREGROUND_BLUE: 1 \ |
150 | | /// FOREGROUND_GREEN: 2 \ |
151 | | /// FOREGROUND_RED: 4 \ |
152 | | /// FOREGROUND_INTENSITY: 8 \ |
153 | | /// BACKGROUND_BLUE: 16 \ |
154 | | /// BACKGROUND_GREEN: 32 \ |
155 | | /// BACKGROUND_RED: 64 \ |
156 | | /// BACKGROUND_INTENSITY: 128 \ |
157 | | /// |
158 | | /// # Example |
159 | | /// |
160 | | /// Default-grey font on muted dark-grey background: |
161 | | /// 4 + 2 + 1 + 128 = `135` |
162 | | /// |
163 | | /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes |
164 | | pub disabled_console_color: u16, |
165 | | /// Controls back- and foreground colors of the client console window |
166 | | /// while it is the currently selected window in the daemon's |
167 | | /// enable/disable submenu. |
168 | | /// |
169 | | /// Uses the same encoding as [`DaemonConfig::console_color`]. |
170 | | /// All [standard Windows color combinations][1] are available: |
171 | | /// |
172 | | /// FOREGROUND_BLUE: 1 \ |
173 | | /// FOREGROUND_GREEN: 2 \ |
174 | | /// FOREGROUND_RED: 4 \ |
175 | | /// FOREGROUND_INTENSITY: 8 \ |
176 | | /// BACKGROUND_BLUE: 16 \ |
177 | | /// BACKGROUND_GREEN: 32 \ |
178 | | /// BACKGROUND_RED: 64 \ |
179 | | /// BACKGROUND_INTENSITY: 128 \ |
180 | | /// |
181 | | /// # Example |
182 | | /// |
183 | | /// Bright-white font on blue background: |
184 | | /// 4 + 2 + 1 + 8 + 16 = `31` |
185 | | /// |
186 | | /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes |
187 | | pub highlighted_console_color: u16, |
188 | | } |
189 | | |
190 | | impl Default for ClientConfig { |
191 | | /// Returns a sensible default `ClientConfig`. |
192 | | /// |
193 | | /// # Returns |
194 | | /// |
195 | | /// `ClientConfig` with the following values: |
196 | | /// * `ssh_config_path` - `%USERPROFILE%\.ssh\config` |
197 | | /// * `program` - `ssh` |
198 | | /// * `arguments` - `-XY {{USERNAME_AT_HOST}}` |
199 | | /// * `username_host_placeholder` - `{{USERNAME_AT_HOST}}` |
200 | | /// * `disabled_console_color` - `135` |
201 | | /// * `highlighted_console_color` - `31` |
202 | | /// |
203 | | /// Note: %USERPROFILE% actually is resolved by us, so the actual value |
204 | | /// is whatever the environment variable at runtime points to. |
205 | 81 | fn default() -> Self { |
206 | 81 | return ClientConfig { |
207 | 81 | ssh_config_path: format!("{}\\.ssh\\config", env::var("USERPROFILE").unwrap()), |
208 | 81 | program: "ssh".to_string(), |
209 | 81 | arguments: vec![ |
210 | 81 | "-XY".to_string(), |
211 | 81 | DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(), |
212 | 81 | ], |
213 | 81 | username_host_placeholder: DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(), |
214 | 81 | disabled_console_color: DEFAULT_DISABLED_CONSOLE_COLOR, |
215 | 81 | highlighted_console_color: DEFAULT_HIGHLIGHTED_CONSOLE_COLOR, |
216 | 81 | }; |
217 | 81 | } |
218 | | } |
219 | | |
220 | | /// Representation of the `client` subcommand configurations |
221 | | /// where everything is optional. |
222 | | #[derive(Serialize, Deserialize)] |
223 | | pub struct ClientConfigOpt { |
224 | | #[allow(missing_docs)] |
225 | | pub ssh_config_path: Option<String>, |
226 | | #[allow(missing_docs)] |
227 | | pub program: Option<String>, |
228 | | #[allow(missing_docs)] |
229 | | pub arguments: Option<Vec<String>>, |
230 | | #[allow(missing_docs)] |
231 | | pub username_host_placeholder: Option<String>, |
232 | | #[allow(missing_docs)] |
233 | | pub disabled_console_color: Option<u16>, |
234 | | #[allow(missing_docs)] |
235 | | pub highlighted_console_color: Option<u16>, |
236 | | } |
237 | | |
238 | | impl Default for ClientConfigOpt { |
239 | 13 | fn default() -> Self { |
240 | 13 | return ClientConfig::default().into(); |
241 | 13 | } |
242 | | } |
243 | | |
244 | | impl From<ClientConfigOpt> for ClientConfig { |
245 | | /// Unwraps the existing configuration values or applies the default. |
246 | 20 | fn from(val: ClientConfigOpt) -> Self { |
247 | 20 | let default = ClientConfig::default(); |
248 | 20 | return ClientConfig { |
249 | 20 | ssh_config_path: val.ssh_config_path.unwrap_or(default.ssh_config_path), |
250 | 20 | program: val.program.unwrap_or(default.program), |
251 | 20 | arguments: val.arguments.unwrap_or(default.arguments), |
252 | 20 | username_host_placeholder: val |
253 | 20 | .username_host_placeholder |
254 | 20 | .unwrap_or(default.username_host_placeholder), |
255 | 20 | disabled_console_color: val |
256 | 20 | .disabled_console_color |
257 | 20 | .unwrap_or(default.disabled_console_color), |
258 | 20 | highlighted_console_color: val |
259 | 20 | .highlighted_console_color |
260 | 20 | .unwrap_or(default.highlighted_console_color), |
261 | 20 | }; |
262 | 20 | } |
263 | | } |
264 | | |
265 | | impl From<ClientConfig> for ClientConfigOpt { |
266 | | /// Wraps all configuration values as options. |
267 | 14 | fn from(val: ClientConfig) -> Self { |
268 | 14 | return ClientConfigOpt { |
269 | 14 | ssh_config_path: Some(val.ssh_config_path), |
270 | 14 | program: Some(val.program), |
271 | 14 | arguments: Some(val.arguments), |
272 | 14 | username_host_placeholder: Some(val.username_host_placeholder), |
273 | 14 | disabled_console_color: Some(val.disabled_console_color), |
274 | 14 | highlighted_console_color: Some(val.highlighted_console_color), |
275 | 14 | }; |
276 | 14 | } |
277 | | } |
278 | | |
279 | | /// Representation of the `daemon` subcommand configurations. |
280 | | #[derive(Serialize, Deserialize, PartialEq, Debug)] |
281 | | pub struct DaemonConfig { |
282 | | /// Height in pixel of the daemon console window. |
283 | | /// |
284 | | /// Note: we are [DPI Unaware][1] which means the number of pixels |
285 | | /// represents the `logical` scale, not the physical. |
286 | | /// |
287 | | /// [1]: https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#dpi-unaware |
288 | | pub height: i32, |
289 | | /// Controls how the client console windows make use of the available screen space. |
290 | | /// |
291 | | /// * `> 0.0` - Aims for vertical rectangle shape. |
292 | | /// The larger the value, the more exaggerated the "verticality". |
293 | | /// Eventually the windows will all be columns. |
294 | | /// * `= 0.0` - Aims for square shape. |
295 | | /// * `< 0.0` - Aims for horizontal rectangle shape. |
296 | | /// The smaller the value, the more exaggerated the "horizontality". |
297 | | /// Eventually the windows will all be rows. |
298 | | /// `-1.0` is the sweetspot for mostly preserving a 16:9 ratio. |
299 | | #[serde(alias = "aspect_ratio_adjustement")] |
300 | | pub aspect_ratio_adjustment: f64, |
301 | | /// Controls back- and foreground colors of the daemon console window. |
302 | | /// |
303 | | /// All [standard Windows color combinations][1] are available: |
304 | | /// |
305 | | /// FOREGROUND_BLUE: 1 \ |
306 | | /// FOREGROUND_GREEN: 2 \ |
307 | | /// FOREGROUND_RED: 4 \ |
308 | | /// FOREGROUND_INTENSITY: 8 \ |
309 | | /// BACKGROUND_BLUE: 16 \ |
310 | | /// BACKGROUND_GREEN: 32 \ |
311 | | /// BACKGROUND_RED: 64 \ |
312 | | /// BACKGROUND_INTENSITY: 128 \ |
313 | | /// |
314 | | /// # Example |
315 | | /// |
316 | | /// White font on red background: 8 + 4 + 2 + 1 + 128 + 64 = `207` |
317 | | /// |
318 | | /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes |
319 | | pub console_color: u16, |
320 | | /// Behavior when an arrow / `hjkl` keystroke would move the |
321 | | /// enable/disable submenu's selection past the edge of the client grid. |
322 | | /// |
323 | | /// * `clamp` (default) - keep the current selection. |
324 | | /// * `wrap` - wrap to the opposite edge of the same row (Left/Right) |
325 | | /// or column (Up/Down). |
326 | | pub submenu_edge_behavior: EdgeBehavior, |
327 | | } |
328 | | |
329 | | impl Default for DaemonConfig { |
330 | | /// Returns a sensible default `DaemonConfig`. |
331 | | /// |
332 | | /// # Returns |
333 | | /// |
334 | | /// `DaemonConfig` with the following values: |
335 | | /// * `height` - `200` |
336 | | /// * `aspect_ratio_adjustment` - `-1.0` |
337 | | /// * `console_color` - `207` |
338 | | /// * `submenu_edge_behavior` - `clamp` |
339 | 79 | fn default() -> Self { |
340 | 79 | return DaemonConfig { |
341 | 79 | height: 200, |
342 | 79 | aspect_ratio_adjustment: -1f64, |
343 | 79 | console_color: (FOREGROUND_INTENSITY |
344 | 79 | | FOREGROUND_RED |
345 | 79 | | FOREGROUND_GREEN |
346 | 79 | | FOREGROUND_BLUE |
347 | 79 | | BACKGROUND_INTENSITY |
348 | 79 | | BACKGROUND_RED) |
349 | 79 | .0, |
350 | 79 | submenu_edge_behavior: EdgeBehavior::default(), |
351 | 79 | }; |
352 | 79 | } |
353 | | } |
354 | | |
355 | | /// Representation of the `daemon` subcommand configurations |
356 | | /// where everything is optional. |
357 | | #[derive(Serialize, Deserialize)] |
358 | | pub struct DaemonConfigOpt { |
359 | | #[allow(missing_docs)] |
360 | | pub height: Option<i32>, |
361 | | #[allow(missing_docs)] |
362 | | #[serde(alias = "aspect_ratio_adjustement")] |
363 | | pub aspect_ratio_adjustment: Option<f64>, |
364 | | #[allow(missing_docs)] |
365 | | pub console_color: Option<u16>, |
366 | | #[allow(missing_docs)] |
367 | | pub submenu_edge_behavior: Option<EdgeBehavior>, |
368 | | } |
369 | | |
370 | | impl Default for DaemonConfigOpt { |
371 | 12 | fn default() -> Self { |
372 | 12 | return DaemonConfig::default().into(); |
373 | 12 | } |
374 | | } |
375 | | |
376 | | impl From<DaemonConfigOpt> for DaemonConfig { |
377 | | /// Unwraps the existing configuration values or applies the default. |
378 | 20 | fn from(val: DaemonConfigOpt) -> Self { |
379 | 20 | let default = DaemonConfig::default(); |
380 | 20 | return DaemonConfig { |
381 | 20 | height: val.height.unwrap_or(default.height), |
382 | 20 | aspect_ratio_adjustment: val |
383 | 20 | .aspect_ratio_adjustment |
384 | 20 | .unwrap_or(default.aspect_ratio_adjustment), |
385 | 20 | console_color: val.console_color.unwrap_or(default.console_color), |
386 | 20 | submenu_edge_behavior: val |
387 | 20 | .submenu_edge_behavior |
388 | 20 | .unwrap_or(default.submenu_edge_behavior), |
389 | 20 | }; |
390 | 20 | } |
391 | | } |
392 | | |
393 | | impl From<DaemonConfig> for DaemonConfigOpt { |
394 | | /// Wraps all configuration values as options. |
395 | 13 | fn from(val: DaemonConfig) -> Self { |
396 | 13 | return DaemonConfigOpt { |
397 | 13 | height: Some(val.height), |
398 | 13 | aspect_ratio_adjustment: Some(val.aspect_ratio_adjustment), |
399 | 13 | console_color: Some(val.console_color), |
400 | 13 | submenu_edge_behavior: Some(val.submenu_edge_behavior), |
401 | 13 | }; |
402 | 13 | } |
403 | | } |
404 | | |
405 | | #[cfg(test)] |
406 | | #[path = "../tests/utils/test_config.rs"] |
407 | | mod test_config; |